import java.util.*;
import java.io.*;

/** Router simulator course work for distributed systems. */
public class myRouter {
	/** The server is listening at this local port. */
	protected int LocalPort;
	/** The port to which the client will connect. */
	protected int Port;
	
	/** The list of the neighboring routers' ips and ports. Constructed such that
	 * neighborIp[i] = ip; neighborIp[i+1] = port; where i is an even integer. */
	public ArrayList<String> neighborIp = new ArrayList<String>();
	/** The list of networks this router is connected to, ie [A,B,C]. */
	public ArrayList<String> Networks = new ArrayList<String>();
	/** The map of client connections we have from this router to its neighbors.
	 *  Constructed using the nieghborIp list. */
	public HashMap<newClient, Thread> Clients = new HashMap<newClient, Thread>();
	/** Holds the router table information for this router. Formatted like [A-0-1.34.53.13:5555, B-0-1.34.53.14:5555]. */
	public Vector<String> routerTable = new Vector<String>();
	public String routerID;
	public String thisIP = null;
	public boolean has_a_connection =false;

	public static void main(String[] args) {
		// ahh no more static messes
		new myRouter(args);
	}
	
	/** Parses through the command line arguments and starts the connection procedures. */
	public myRouter(String[] args) {
		try {
			// we later will create a server that listens on this port
			this.LocalPort = Integer.parseInt(args[0]);
			
			// the list of networks we are attached to, in the form of string names
			for (int i=0; i<args[1].length(); ++i) {
				this.Networks.add(args[1].substring(i, i+1));
			}
			
			// and finally a list of the other routers this process is connected to
			String[] nets = args[2].split("::"), tmp;
			for (int i=0; i<nets.length; ++i) {
				tmp = nets[i].split(":");
				this.neighborIp.add(tmp[0]);
				this.neighborIp.add(tmp[1]);
			}
			
			this.start();
		}
		catch (IndexOutOfBoundsException e) {
			System.err.println("Error: Not enough or malformed arguments. Try -h for help or use the format LocalPort ABC Ip:Port");
			e.printStackTrace();
		}
	}

	public synchronized void setLocalIP(String o){
		this.thisIP = o;
	}
	
	/** Creates the new client and server threads using the information
	 * 	parsed from the command line and stores them. */
	public void start(){
		// run clients and keep them in a list for later access
		for (int i=0; i<this.neighborIp.size(); i+=2){
			newClient tmpClient = new newClient(this.neighborIp.get(i), Integer.parseInt(this.neighborIp.get(i+1)));
			Thread clientThread = new Thread(tmpClient);
			clientThread.start();
			this.Clients.put(tmpClient, clientThread);
		}
		
		System.out.println( "Server is listening on: " + this.LocalPort + " Clients number " + (this.neighborIp.size()/2) );
		new Listener(this.LocalPort, this).start();
		while(!has_a_connection){	
			try { Thread.sleep(1000); } catch (InterruptedException e) {e.printStackTrace();}
		}
		try {this.doRouter();} catch (IOException e) {e.printStackTrace();}
	}
	
	/**
	 * Builds the initial table, including the first line, which is the
	 * router's ip:port, with no delimiters.
	 */
	public void initTable() {
		this.routerID = this.thisIP + ":" + this.LocalPort;
		this.routerTable.add(this.routerID);
		for (String network: this.Networks) {
			this.routerTable.add(network + "-0-" + this.routerID);
		}
	}
	
	public synchronized void syncTable(Vector<String> table) {
		String[] thisrouter, otherrouter;
		String otherID = table.get(0);
		
		for (int i=1; i<this.routerTable.size(); i++) {
			thisrouter = this.routerTable.get(i).split("-");
			
			for (int j=1; j<table.size(); j++) {
				otherrouter = table.get(j).split("-");
				
				// make sure we are not already directly connected to networks in
				// the other routers table
				if ( !this.Networks.contains(otherrouter[0]) ) {
					
					// found a matching entry that offers fewer hops:
					// then assign the new hops value and assign the route
					// to the ip:port of the router that sent us this table
					if ( thisrouter[0].equals(otherrouter[0]) && 
					Integer.parseInt(thisrouter[1]) < Integer.parseInt(otherrouter[1])) {
						thisrouter[1] = otherrouter[1];
						thisrouter[2] = otherID;
					}
					
					// if there is a new network in the other router's table add it to
					// the current router's table
					if ( this.isNew(otherrouter[0]) ) {
						String tmp = otherrouter[0] + "-" + otherrouter[1] + "-" + otherID; 
						this.routerTable.add(tmp);
					}
				}
			}
		}
		
		//System.out.println("from syncTable: " + this.routerTable);
	}
	
	/**
	 * Helps determine if the network is in the current router's table.
	 * @param network
	 * @return True if the given network is not in the current router table, false otherwise.
	 */
	public boolean isNew(String network) {
		String[] thisrouter;
		for (String thistmp: this.routerTable) {
			thisrouter = thistmp.split("-");
			
			if ( network.equals(thisrouter[0]) ) {
				return false;
			}
		}
		
		return true;
	}
	
	public void doRouter() throws IOException{
//		try {
//			for (newClient client: this.Clients.keySet()) {
//				while (client.isConnected == false) {
//					Thread.sleep(50);
//				}
//				client.out.writeUTF("hello");
//				System.out.print("client said: " + client.in.readUTF());
//			}
//		}
//		catch (InterruptedException e) {
//			e.printStackTrace();
//		}
		
		initTable();
		int sleepTime = 5000;
		int iterSleep = 50;
		int i = 0;
		
		System.out.println("Waiting for the table to converge...");
		while ( iterSleep*i <= sleepTime ) {
			for (newClient client: this.Clients.keySet()) {
				for (String line: this.routerTable) {
					if ( client.isConnected && Clients.get(client).isAlive())
						client.out.writeUTF(line);
				}
				if( client.isConnected && Clients.get(client).isAlive())
					client.out.writeUTF("DONE");
			}
			
			try { Thread.sleep(iterSleep); } catch (InterruptedException e) {e.printStackTrace();}
			++i;
			//System.out.println("from doRouter: " + this.routerTable);
		}
		
		String[] table;
		String direct = this.routerTable.get(0);
		String msg;
		System.out.println("\nResults:");
		for (i=1; i<this.routerTable.size(); ++i) {
			table = this.routerTable.get(i).split("-");
			
			msg = table[0] + " " + table[1] + " ";
			if ( table[2].equals(direct)) msg += "direct";
			else msg += direct;
			
			System.out.println(msg);
		}
		
		System.exit(0);
	}
}